package net.aiion.weave.impl.painters.stream.identicons;

import java.awt.Color;
import java.awt.Graphics2D;
import java.awt.Shape;
import java.awt.geom.AffineTransform;
import java.awt.geom.Path2D;
import java.awt.geom.Point2D;
import java.awt.geom.Rectangle2D;
import java.awt.image.BufferedImage;
import java.io.IOException;
import java.io.OutputStream;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Arrays;

import javax.imageio.ImageIO;

import net.aiion.weave.spi.painters.stream.identicons.Identicon;

public final class IdenticonImpl implements Identicon
{
    private static final int MIN_SIZE = 24;
    private static final int DEFAULT_SIZE = 48;

    private static final int SHAPE1_HASH_INDEX = 6;
    private static final int SHAPE2_HASH_INDEX = 7;
    private static final int SHAPE3_HASH_INDEX = 8;
    private static final int SHAPE4_HASH_INDEX = 9;

    /*
     * LT +  PT CT +  +  RT
     *
     * +  +  +  +  +  +  +
     * 
     * LP +  PP +  QP +  +
     * 
     * LC +  +  CC +  +  RC
     * 
     * LQ +  PQ +  QQ +  RQ
     * 
     * +  +  +  +  +  +  +
     * 
     * LB +  PB CB +  +  RB 
     */

    private static final Point2D.Double[][] SHAPES;

    private static final int POINT_LT = 0x00;
    private static final int POINT_LP = 0x02;
    private static final int POINT_LC = 0x03;
    private static final int POINT_LQ = 0x04;
    private static final int POINT_LB = 0x06;
    private static final int POINT_PT = 0x20;
    private static final int POINT_PP = 0x22;
    private static final int POINT_PQ = 0x24;
    private static final int POINT_PB = 0x26;
    private static final int POINT_CT = 0x30;
    private static final int POINT_CC = 0x33;
    private static final int POINT_CB = 0x36;
    private static final int POINT_QP = 0x42;
    private static final int POINT_QQ = 0x44;
    private static final int POINT_RT = 0x60;
    private static final int POINT_RC = 0x63;
    private static final int POINT_RQ = 0x64;
    private static final int POINT_RB = 0x66;

    private static final int POINT_Y_MASK = 0x0F;
    private static final int POINT_X_MASK = 0xF0;
    private static final int POINT_X_SHIFT_OFFSET = 0x04;

    private static final int POINT_MAX = 6;

    private static final int[][] SHAPES_ENCODED_POINTS = new int[][] {
        new int[] {
            POINT_LT, POINT_RT, POINT_CC, POINT_RC, POINT_CB, POINT_CC, POINT_LB, POINT_LT
        },
        new int[] {
            POINT_LT, POINT_CT, POINT_RC, POINT_CB, POINT_LB, POINT_CC, POINT_LT
        },
        new int[] {
            POINT_LT, POINT_CC, POINT_LC, POINT_CB, POINT_CC, POINT_RB, POINT_RC, POINT_CC, POINT_CT, POINT_LT
        },
        new int[] {
            POINT_LT, POINT_RT, POINT_RB, POINT_LB, POINT_RC, POINT_LT
        },
        new int[] {
            POINT_LT, POINT_RT, POINT_LC, POINT_LT
        },
        new int[] {
            POINT_CT, POINT_RT, POINT_RB, POINT_LB, POINT_LC, POINT_RB, POINT_CT
        },
        new int[] {
            POINT_LT, POINT_PT, POINT_RB, POINT_LP, POINT_LT
        },
        new int[] {
            POINT_LT, POINT_RC, POINT_CB, POINT_LT
        },
        new int[] {
            POINT_LT, POINT_RT, POINT_RB, POINT_LB, POINT_CC, POINT_LT
        },
        new int[] {
            POINT_LT, POINT_CT, POINT_LC, POINT_CB, POINT_LB, POINT_LT
        },
        new int[] {
            POINT_LT, POINT_CT, POINT_LC, POINT_CB, POINT_RC, POINT_RB, POINT_LB, POINT_LT
        },
        new int[] {
            POINT_LT, POINT_RT, POINT_LC, POINT_RC, POINT_LB, POINT_LT
        },
        new int[] {
            POINT_CT, POINT_RC, POINT_CC, POINT_CB, POINT_LC, POINT_CT
        },
        new int[] {
            POINT_LC, POINT_LB, POINT_RT, POINT_LC
        },
        new int[] {
            POINT_LT, POINT_PT, POINT_RC, POINT_PB, POINT_LB, POINT_LT
        },
        new int[] {
            POINT_LT, POINT_PT, POINT_PP, POINT_QP, POINT_QQ, POINT_RQ, POINT_RB, POINT_PB, POINT_PQ, POINT_LQ,
            POINT_LT
        }
    };

    private static final int BYTE_MAX_VALUE = 0xFF;

    private static final int SHAPE_MASK = 0x0F;
    private static final int ROTATION_MASK = 0x30;
    private static final int ROTATION_SHIFT_OFFSET = 4;

    private static final int INVERSION_MASK = 0x40;
    private static final int FOREGROUND_COLOR_MASK = 0x80;

    private static final int FOREGROUND_COLOR_1_R_HASH_INDEX = 0;
    private static final int FOREGROUND_COLOR_1_G_HASH_INDEX = 1;
    private static final int FOREGROUND_COLOR_1_B_HASH_INDEX = 2;

    private static final int FOREGROUND_COLOR_2_R_HASH_INDEX = 3;
    private static final int FOREGROUND_COLOR_2_G_HASH_INDEX = 4;
    private static final int FOREGROUND_COLOR_2_B_HASH_INDEX = 5;
    private static final int FOREGROUND_COLOR_2_ALPHA = 0x77;

    private static final int ROTATION_ANGLE_DEGREES = -90;

    private int identiconSize;

    private Color backgroundColor;

    private boolean rotationActive;

    private Color foregroundColor1;
    private Color foregroundColor2;

    private byte[] identiconData;

    private Graphics2D graphics2D;

    public IdenticonImpl() {
        this(new byte[0]);
    }

    public IdenticonImpl(final String aIdenticonData) {
        this(aIdenticonData.getBytes());
    }

    public IdenticonImpl(final byte[] aIdenticonData) {
        setSize(DEFAULT_SIZE);
        enableRotation();
        setData(aIdenticonData);
    }

    @Override
    public void setSize(final int aIdenticonSize) {
        if (aIdenticonSize < MIN_SIZE) {
            identiconSize = MIN_SIZE;
        } else {
            identiconSize = aIdenticonSize;
        }
    }

    @Override
    public int getSize() {
        return identiconSize;
    }

    @Override
    public void setBackgroundColor(final Color aBackgroundColor) {
        backgroundColor = aBackgroundColor;
    }

    @Override
    public Color getBackgroundColor() {
        return backgroundColor;
    }

    @Override
    public void setForegroundColor(final Color aForegroundColor) {
        foregroundColor1 = aForegroundColor;
    }

    @Override
    public Color getForegroundColor() {
        return foregroundColor1;
    }

    @Override
    public void enableRotation() {
        rotationActive = true;
    }

    @Override
    public void disableRotation() {
        rotationActive = false;
    }

    @Override
    public boolean rotationActive() {
        return rotationActive;
    }

    @Override
    public void setData(final String aIdenticonData) {
        setData(aIdenticonData.getBytes());
    }

    @Override
    public void setData(final byte[] aIdenticonData) {
        identiconData = Arrays.copyOf(aIdenticonData, aIdenticonData.length);
    }

    @Override
    public void drawIdenticon(final OutputStream aOutputStream) throws IOException {
        try {
            final BufferedImage bufferedImage = new BufferedImage(identiconSize, identiconSize,
                BufferedImage.TYPE_INT_ARGB);
            final double scaleFactor = calculateScaleFactor();
            graphics2D = bufferedImage.createGraphics();
            drawIdenticon(calculateMessageDigest(), scaleFactor);
            ImageIO.write(bufferedImage, "png", aOutputStream);
        } catch (final NoSuchAlgorithmException e) {
            throw new IOException(e);
        } finally {
            graphics2D = null;
        }
    }

    private double calculateScaleFactor() {
        double scaleFactor = identiconSize / 2d;
        if (rotationActive()) {
            scaleFactor = scaleFactor / 2d;
        }
        return scaleFactor;
    }

    private byte[] calculateMessageDigest() throws NoSuchAlgorithmException {
        final MessageDigest messageDigest = MessageDigest.getInstance("SHA-1");
        messageDigest.update(identiconData);
        final byte[] sha1Hash = messageDigest.digest();
        final byte[] sha1HashCompressed = new byte[sha1Hash.length / 2];
        for (int i = 0; i < sha1HashCompressed.length; i++) {
            sha1HashCompressed[i] = (byte) (sha1Hash[i * 2] ^ sha1Hash[i * 2 + 1]);
        }
        return sha1HashCompressed;
    }

    private void drawIdenticon(final byte[] hashBytes, final double scaleFactor) {
        loadForegroundColors(hashBytes);
        drawBackground();
        drawShapes(hashBytes, scaleFactor);
    }

    private void loadForegroundColors(final byte[] aHashBytes) {
        if (foregroundColor1 == null) {
            loadForegroundColorsFromHashCode(aHashBytes);
        } else {
            loadForegroundColorsFromFirstForegroundColor();
        }

        if (backgroundColor == null) {
            setBackgroundColor(createComplementaryColor(foregroundColor1));
        }
    }

    private void loadForegroundColorsFromHashCode(final byte[] aHashBytes) {
        foregroundColor1 = new Color(aHashBytes[FOREGROUND_COLOR_1_R_HASH_INDEX] & BYTE_MAX_VALUE,
            aHashBytes[FOREGROUND_COLOR_1_G_HASH_INDEX] & BYTE_MAX_VALUE, aHashBytes[FOREGROUND_COLOR_1_B_HASH_INDEX]
                & BYTE_MAX_VALUE);
        foregroundColor2 = new Color(aHashBytes[FOREGROUND_COLOR_2_R_HASH_INDEX] & BYTE_MAX_VALUE,
            aHashBytes[FOREGROUND_COLOR_2_G_HASH_INDEX] & BYTE_MAX_VALUE, aHashBytes[FOREGROUND_COLOR_2_B_HASH_INDEX]
                & BYTE_MAX_VALUE);
    }

    private void loadForegroundColorsFromFirstForegroundColor() {
        foregroundColor2 = new Color(foregroundColor1.getRed(), foregroundColor1.getGreen(),
            foregroundColor1.getBlue(), FOREGROUND_COLOR_2_ALPHA);
    }

    private Color createComplementaryColor(final Color c) {
        return new Color(BYTE_MAX_VALUE - c.getRed(), BYTE_MAX_VALUE - c.getGreen(), BYTE_MAX_VALUE - c.getBlue());
    }

    private void drawBackground() {
        graphics2D.setColor(backgroundColor);
        graphics2D.fillRect(0, 0, identiconSize, identiconSize);
    }

    private void drawShapes(final byte[] hashBytes, final double scaleFactor) {
        drawShape(0, 0, scaleFactor, hashBytes[SHAPE1_HASH_INDEX] & BYTE_MAX_VALUE);
        drawShape(1, 0, scaleFactor, hashBytes[SHAPE2_HASH_INDEX] & BYTE_MAX_VALUE);
        drawShape(0, 1, scaleFactor, hashBytes[SHAPE3_HASH_INDEX] & BYTE_MAX_VALUE);
        drawShape(1, 1, scaleFactor, hashBytes[SHAPE4_HASH_INDEX] & BYTE_MAX_VALUE);
    }

    private void drawShape(final int x, final int y, final double scaleFactor, final int shapeBits) {
        Shape s = createShape(SHAPES[shapeBits & SHAPE_MASK], x, y, scaleFactor);

        final int rotationAngle = ((shapeBits & ROTATION_MASK) >>> ROTATION_SHIFT_OFFSET) * 90;
        if (rotationAngle != 0) {
            final Rectangle2D b = s.getBounds2D();

            AffineTransform affineTransform = new AffineTransform();
            affineTransform.translate(-b.getX() - scaleFactor / 2, -b.getY() - scaleFactor / 2);
            s = affineTransform.createTransformedShape(s);

            affineTransform = new AffineTransform();
            affineTransform.rotate(Math.toRadians(rotationAngle));
            s = affineTransform.createTransformedShape(s);

            affineTransform = new AffineTransform();
            affineTransform.translate(b.getX() + scaleFactor / 2, b.getY() + scaleFactor / 2);
            s = affineTransform.createTransformedShape(s);
        }

        final int inversionFlags = shapeBits & INVERSION_MASK;

        final Color foregroundColor;
        if ((shapeBits & FOREGROUND_COLOR_MASK) == 0) {
            foregroundColor = foregroundColor1;
        } else {
            foregroundColor = foregroundColor2;
        }

        if (inversionFlags != 0) {
            graphics2D.setColor(foregroundColor);
            drawShape(new Rectangle2D.Double(x * scaleFactor, y * scaleFactor, scaleFactor, scaleFactor));

            graphics2D.setColor(backgroundColor);
            drawShape(s);
        } else {
            graphics2D.setColor(foregroundColor);
            drawShape(s);
        }
    }

    private void drawShape(final Shape s) {
        graphics2D.fill(s);

        if (rotationActive()) {
            drawShapeRotated(drawShapeRotated(drawShapeRotated(s)));
        }
    }

    private Shape drawShapeRotated(final Shape s) {
        final Shape rts = translateShape(rotateShape(s), identiconSize);
        graphics2D.fill(rts);
        return rts;
    }

    private Shape rotateShape(final Shape s) {
        final AffineTransform at = new AffineTransform();
        at.rotate(Math.toRadians(ROTATION_ANGLE_DEGREES));
        return at.createTransformedShape(s);
    }

    private Shape translateShape(final Shape s, final double dy) {
        final AffineTransform at = new AffineTransform();
        at.translate(0, dy);
        return at.createTransformedShape(s);
    }

    private Path2D.Double createShape(final Point2D.Double[] shapePoints, final int xOffset, final int yOffset,
        final double scaleFactor)
    {
        final Path2D.Double resultPath = new Path2D.Double();

        resultPath.moveTo((shapePoints[0].x + xOffset) * scaleFactor, (shapePoints[0].y + yOffset) * scaleFactor);
        for (int i = 1; i < shapePoints.length; i++) {
            resultPath.lineTo((shapePoints[i].x + xOffset) * scaleFactor, (shapePoints[i].y + yOffset) * scaleFactor);
        }

        return resultPath;
    }

    static {
        SHAPES = new Point2D.Double[SHAPES_ENCODED_POINTS.length][];
        for (int i = 0; i < SHAPES_ENCODED_POINTS.length; i++) {
            SHAPES[i] = createPolygon(SHAPES_ENCODED_POINTS[i]);
        }
    }

    private static Point2D.Double[] createPolygon(final int... aEncodedPoints) {
        final Point2D.Double[] polygonPoints = new Point2D.Double[aEncodedPoints.length];
        polygonPoints[0] = createPoint(aEncodedPoints[0]);
        for (int i = 1; i < aEncodedPoints.length; i++) {
            polygonPoints[i] = createPoint(aEncodedPoints[i]);
        }
        return polygonPoints;
    }

    private static Point2D.Double createPoint(final int aEncodedPoint) {
        return new Point2D.Double(((aEncodedPoint & POINT_X_MASK) << POINT_X_SHIFT_OFFSET) / POINT_MAX,
            (aEncodedPoint & POINT_Y_MASK) / POINT_MAX);
    }
}
